<template>
  <el-upload
    action="/bigFile/upload"
    :data="data"
    :auto-upload="false"
    :accept="accept"
    :limit="limit"
    :multiple="multiple"
    :disabled="disabled"
    :show-file-list="false"
    :on-change="handleFileChange"
    ref="upload"
  >
    <slot>
      <div v-if="fileName && !multiple">
        <span
          class="f-14"
          v-if="fileName"
        >{{ fileName }}</span>
        <i
          class="el-icon-circle-close m-l-mini"
          @click.stop="clearFile"
        />
      </div>
      <el-button
        v-else
        size="small"
        type="primary"
        plain
        icon="el-icon-upload2"
      >
        上传文件
      </el-button>
    </slot>
  </el-upload>
</template>

<script>
import SparkMD5 from 'spark-md5'
import { mapState } from 'vuex'
import { v4 as uuidv4 } from 'uuid'

async function fileToArrayBuffer (file) {
  return new Promise((resolve, reject) => {
    const fr = new FileReader()
    fr.onload = e => {
      resolve(e.target.result)
    }
    fr.onerror = e => {
      reject(new Error('转换文件格式发生错误', e))
    }
    fr.readAsArrayBuffer(file)
  })
}

export default {
  name: 'BKSegmentUpload',
  props: {
    accept: {
      type: String,
      default: ''
    },
    data: {
      type: Object,
      default: null
    },
    fileType: {
      type: String,
      default: 'video',
      validator (v) {
        return 'image,video,720,orthoimage,dem,dsm,3d,vector,laser,pdf'.split(',').includes(v)
      }
    },
    loading: {
      type: Boolean,
      default: false
    },
    multiple: {
      type: Boolean,
      default: false
    },
    extraParams: {
      type: Object,
      default: () => ({})
    }
  },
  data () {
    return {
      fileName: '',
      // 切片大小 50M
      chunkSize: 50 * 1024 * 1024,
      // 校验切片是否一致使用的数据大小
      bufferSize: 3 * 1024 * 1024,
      // 单个文件同时发送的最大请求数
      maxReqCount: 3,
      // 最大同时上传的文件个数
      maxFileCount: window._maxFileCount || 2,
      // 待上传文件列表
      uploadFileList: [],
      // 正在上传文件列表
      currentFiles: [],
      // 正在上传的文件已经上传了的切片MD5信息
      uploadingFileMd5: {}
    }
  },
  computed: {
    ...mapState('settings', ['theme']),
    limit () {
      return this.multiple ? null : 1
    },
    disabled () {
      return this.multiple ? false : !!this.fileName
    },
    isMultiFileMode () {
      return this.theme.hasFileManager && this.multiple
    }
  },
  methods: {
    async handleFileChange (file) {
      const passed = this.checkFileType(file)
      if (!passed) return
      const fileInfo = { id: uuidv4(), file, params: { ...this.extraParams } }
      this.startUploading(fileInfo)
      if (this.isMultiFileMode) {
        if (this.currentFiles.length < this.maxFileCount) {
          this.currentFiles.push(fileInfo)
        } else {
          this.uploadFileList.push(fileInfo)
          return
        }
      }
      this.$emit('update:loading', true)
      this.beginUpload(fileInfo)
    },
    async beginUpload (fileInfo) {
      try {
        const file = fileInfo.file
        fileInfo.fileSize = file.size
        fileInfo.uploadedSize = 0
        fileInfo.uploadedChunkCount = 0
        if (file.size <= this.chunkSize) {
          await this.splitFile(fileInfo, file.size, 1, file.raw)
        } else {
          await this.splitFile(fileInfo, this.chunkSize, Math.ceil(file.size / this.chunkSize), file.raw)
        }
        this.uploadFile(fileInfo)
      } catch (err) {
        console.log('err', err)
      }
    },
    checkFileType (file) {
      if (file.size / 1024 / 1024 / 1024 > 100) {
        this.$message.error('上传文件大小不能超过100G')
        return false
      }
      if (!this.accept) return true
      const index = file.name.lastIndexOf('.')
      const suffix = file.name.slice(index + 1).toLowerCase()
      const suffixList = this.accept.split(',').map(s => s.trim().slice(1))
      for (const s of suffixList) {
        if (s === suffix) {
          return true
        }
      }
      if (this.accept === '.mp4, .mov') {
        this.$message.error('上传视频仅支持 mp4, mov 格式的视频文件')
      } else if (this.accept === '.tif') {
        this.$message.error('上传文件仅支持 tif 格式的文件')
      } else if (this.accept === '.zip') {
        this.$message.error('上传文件仅支持 zip 格式的文件')
      }
      return false
    },
    async splitFile (fileInfo, chunkSize, chunkCount, rawFile) {
      fileInfo.chunkCount = chunkCount
      let current = 0
      const chunkInfoList = []
      let firstMd5 = ''
      for (let i = 0; i < chunkCount; i++) {
        const spark = new SparkMD5.ArrayBuffer()
        const file = rawFile.slice(current, current + chunkSize)
        // 截取每个片区的前面 3MB 作为 buffer
        const buffer = await fileToArrayBuffer(file.slice(0, this.bufferSize))
        spark.append(buffer)
        const hash = spark.end()
        if (i === 0) {
          firstMd5 = hash
        }
        const item = {
          file: new File([file], hash),
          branchPath: this.data.branchPath,
          chunks: chunkCount,
          chunk: i + 1,
          everyMd5: hash,
          fileName: rawFile.name,
          firstMd5: firstMd5
        }
        chunkInfoList.push(item)
        current += chunkSize
      }
      fileInfo.chunkInfoList = chunkInfoList
      fileInfo.chunkUploadedSizeList = chunkInfoList.map(r => 0)
    },
    async uploadFile (fileInfo) {
      // 校验文件是否存在
      const firstChunk = fileInfo.chunkInfoList[0]
      const checkResult = await this.$request.upload('/bigFile/parallel/checkFile', {
        branchPath: firstChunk.branchPath,
        fileName: firstChunk.fileName,
        firstMd5: firstChunk.firstMd5
      })
      if (checkResult.state === 4) {
        // 上传过的文件直接完成上传操作，抛出结果
        this.finishUploading(fileInfo, checkResult)
        return
      }
      if (checkResult.state === 0) {
        // 此文件没有上传完毕
        this.uploadingFileMd5[fileInfo.id] = checkResult.cfg
      }
      const send = (fileInfo, index, isCheck) => {
        return this.sendRequest(fileInfo, index - 1, isCheck).then((res) => {
          if (res.state === 1) {
            // 切片文件已经存在，但没传完，需要重新传
            return send(fileInfo, index, 'N')
          } else if (res.state === 2) {
            // 当前分片上传完成
            fileInfo.uploadedChunkCount++
            this.checkFinish(fileInfo)
          } else if (res.state === 0) {
            // 当前切片从未上传过，需要上传
            return send(fileInfo, index, 'N')
          }
        })
      }
      const reqs = []
      try {
        for (let i = 1; i <= fileInfo.chunkInfoList.length; i++) {
          // 先判断是否已经上传过
          if (this.uploadingFileMd5[fileInfo.id][i.toString()] && this.uploadingFileMd5[fileInfo.id][i.toString()] === fileInfo.chunkInfoList[i - 1].everyMd5) {
            fileInfo.uploadedChunkCount++
            fileInfo.chunkUploadedSizeList[i - 1] = this.chunkSize
            this.updateProgress(fileInfo)
            if (i === fileInfo.chunkInfoList.length) {
              this.checkFinish(fileInfo)
              return
            }
            continue
          }
          // 在请求队列中移除当前执行完毕的请求
          const e = send(fileInfo, i, 'Y').then(() => reqs.splice(reqs.indexOf(e), 1))
          reqs.push(e)
          if (reqs.length >= this.maxReqCount) {
            await Promise.race(reqs)
          }
        }
      } catch (error) {
        console.log(error)
      }
    },
    // 更新进度条状态
    updateProgress (fileInfo) {
      let uploaded = 0
      for (const size of fileInfo.chunkUploadedSizeList) {
        uploaded += size
      }
      const percentage = Math.max(0, Math.min(100, Math.round(uploaded * 100 / fileInfo.fileSize)))
      if (this.isMultiFileMode) {
        this.$store.commit('utils/updateUploadFileState', {
          id: fileInfo.id,
          // 等待合并完成再设置上传完成
          percentage: Math.min(99, percentage)
        })
      } else {
        this.$store.commit('utils/setFileStateInfo', {
          visible: percentage < 100,
          text: '正在上传文件，请耐心等待',
          percentage
        })
      }
    },
    sendRequest (fileInfo, i, isCheck) {
      const data = {
        ...fileInfo.chunkInfoList[i],
        check: isCheck
      }
      if (isCheck === 'Y') {
        delete data.file
      }
      return this.$request.upload('/bigFile/parallel/upload', data, isCheck === 'Y' ? null : {
        onUploadProgress: (e) => {
          fileInfo.chunkUploadedSizeList[i] = e.loaded
          this.updateProgress(fileInfo)
        }
      })
    },
    async checkFinish (fileInfo) {
      if (fileInfo.uploadedChunkCount < fileInfo.chunkCount) return
      const firstChunk = fileInfo.chunkInfoList[0]
      const res = await this.$request.upload('/bigFile/parallel/mergeFile', {
        branchPath: firstChunk.branchPath,
        chunks: firstChunk.chunks,
        fileName: firstChunk.fileName,
        firstMd5: firstChunk.firstMd5
      })
      this.finishUploading(fileInfo, res)
    },
    startUploading (fileInfo) {
      if (this.isMultiFileMode) {
        this.$store.commit('utils/updateUploadFileState', {
          id: fileInfo.id,
          fileName: fileInfo.file.name,
          fileSize: fileInfo.file.size,
          fileType: this.fileType,
          percentage: 0
        })
      } else {
        this.$store.commit('utils/setFileStateInfo', {
          visible: true,
          text: '正在上传文件，请耐心等待',
          percentage: 0
        })
      }
    },
    finishUploading (fileInfo, res) {
      this.$emit('update:loading', false)
      // 清除分片的MD5信息
      delete this.uploadingFileMd5[fileInfo.id]
      if (this.isMultiFileMode) {
        this.$emit('change', res, fileInfo.params)
        this.$store.commit('utils/updateUploadFileState', {
          id: fileInfo.id,
          percentage: 100
        })
        this.currentFiles.splice(this.currentFiles.indexOf(fileInfo), 1)
        if (this.uploadFileList.length) {
          const nextFile = this.uploadFileList.shift()
          this.currentFiles.push(nextFile)
          this.beginUpload(nextFile)
        } else {
          this.$emit('finish')
        }
      } else {
        this.$emit('change', res)
        this.$message.success('上传成功')
        this.fileName = res.originalFilename
        this.$store.commit('utils/setFileStateInfo', {
          visible: false
        })
      }
    },
    clearFile () {
      this.fileName = ''
      this.$refs.upload.clearFiles()
      this.$emit('clearFile')
    }
  },
  created () {
    this.uploadFileList = []
    this.currentFiles = []
  }
}
</script>
